Introduction¶
Concrete is a widely used construction material known for its high compressive strength and durability. The compressive strength of concrete is crucial in the design of concrete structures, as it indicates the material's ability to withstand compression loads, such as those experienced by columns, foundations, and pavements. This strength is influenced by various factors including the concrete's composition, ingredient proportions, mixing methods, and curing conditions (Nawy, 2008).
The project aims to analyze the Cement Manufacturing Dataset from the UCI Machine Learning Repository to explore the relationships between concrete mix design and compressive strength. This dataset contains information on the ingredients used in concrete mixtures, including cement, blast furnace slag, fly ash, water, superplasticizer, coarse aggregate, fine aggregate, and the concrete's age, with the target variable being the measured compressive strength in MPa. Understanding the factors affecting concrete strength is essential for optimizing mix designs to meet specific strength requirements, and statistical and machine learning techniques can be applied to uncover patterns and build predictive models (Siddique et al., 2011; Khademi et al., 2016).
Research in the field has shown the application of advanced machine learning approaches to predict the compressive strength of concrete containing supplementary cementitious materials. Additionally, studies have utilized machine learning models to predict the strength properties of concrete, highlighting the potential for these techniques to optimize concrete performance (Ahmad et al., 2021). Furthermore, the use of machine learning models to predict the slump, VEBE, and compaction factor of concrete has been explored, emphasizing the versatility of these models in assessing concrete properties (Al-Hashem et al., 2022).
In conclusion, the investigation into the factors influencing concrete compressive strength is crucial for optimizing mix designs and improving the quality and efficiency of concrete construction. The application of statistical and machine learning techniques to analyze the Cement Manufacturing Dataset from the UCI Machine Learning Repository presents an opportunity to gain valuable insights into concrete mix design and strength prediction.
Background Information¶
The study of how strong concrete is against pressure has been a key part of building and civil engineering history. This strength is crucial for ensuring buildings and other structures can stand up to the test of time and the elements. The dataset used in this research reflects the long-standing efforts to understand what makes concrete strong by looking at the different materials that go into it.
The mix of water, cement, and other materials like fly ash and slag, which can change how concrete cures and how tough it is, are not just numbers in a table. They tell a story about concrete and its role in safe and lasting construction. These ingredients are essential for fine-tuning the makeup of concrete to meet the demands of different building projects.
Despite a lot of study, predicting the strength of concrete is still a complex issue because of the unpredictable ways its components interact. This research addresses this challenge by using machine learning, which is well-suited for understanding such complex patterns. Machine learning algorithms can find hidden trends in data that might be missed by traditional statistical methods.
Using machine learning to look into concrete's properties is not only for academic research. It has real-world uses, helping engineers make better concrete mixtures. This research adds to the foundational knowledge of how to build strong structures and plays a part in shaping the future of construction. It merges lessons from the past with the latest in data analysis, showing how a deep dive into data can lead to smarter building methods and stronger materials.
Research Questions¶
The analysis of the Concrete Compressive Strength dataset aims to address the following key research inquiries:
- How do individual components, including cement, blast furnace slag, fly ash, water, superplasticizer, coarse aggregate, and fine aggregate, impact the compressive strength of concrete?
- What is the relative significance of each component in influencing the ultimate compressive strength?
- Can we accurately classify concrete compressive strength as high, medium, or low based on the composition and types of mixture components?
- What are the critical features or component combinations that distinguish the categories of high, medium, and low compressive strength?
Data Source and Collection¶
The data used for this analysis on Concrete Compressive Strength will be sourced from the UCI Machine Learning Repository's open data portal. This dataset was generously provided by Prof. I-Cheng Yeh from the Department of Information Management at Chung-Hua University in Hsin Chu, Taiwan.
This dataset can be accessed through the following link: Concrete Compressive Strength Dataset.
Data Description¶
The dataset used for the machine learning project contains various variables, which are described below. For each variable, the name, type, measurement unit, and a brief description are given. The order of this listing corresponds to the sequence of numerical values in the database columns.
Variable Description¶
| Variable | Type | Unit | Description |
|---|---|---|---|
| Cement | quantitative | kg in a m³ mixture | Input variable |
| Blast Furnace Slag | quantitative | kg in a m³ mixture | Input variable |
| Fly Ash | quantitative | kg in a m³ mixture | Input variable |
| Water | quantitative | kg in a m³ mixture | Input variable |
| Superplasticizer | quantitative | kg in a m³ mixture | Input variable |
| Coarse Aggregate | quantitative | kg in a m³ mixture | Input variable |
| Fine Aggregate | quantitative | kg in a m³ mixture | Input variable |
| Age | quantitative | Days (1-365) | Input variable |
| Concrete Compressive Strength | quantitative | MPa | Output variable |
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import scipy
import openpyxl
import xlrd
Upload data¶
Load data into Pandas dataframe¶
df = pd.read_csv('Concrete_Data.csv')
Initial Data Preview¶
df.head()
| Cement (component 1)(kg in a m^3 mixture) | Blast Furnace Slag (component 2)(kg in a m^3 mixture) | Fly Ash (component 3)(kg in a m^3 mixture) | Water (component 4)(kg in a m^3 mixture) | Superplasticizer (component 5)(kg in a m^3 mixture) | Coarse Aggregate (component 6)(kg in a m^3 mixture) | Fine Aggregate (component 7)(kg in a m^3 mixture) | Age (day) | Concrete compressive strength(MPa, megapascals) | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 540.0 | 0.0 | 0.0 | 162.0 | 2.5 | 1040.0 | 676.0 | 28 | 79.99 |
| 1 | 540.0 | 0.0 | 0.0 | 162.0 | 2.5 | 1055.0 | 676.0 | 28 | 61.89 |
| 2 | 332.5 | 142.5 | 0.0 | 228.0 | 0.0 | 932.0 | 594.0 | 270 | 40.27 |
| 3 | 332.5 | 142.5 | 0.0 | 228.0 | 0.0 | 932.0 | 594.0 | 365 | 41.05 |
| 4 | 198.6 | 132.4 | 0.0 | 192.0 | 0.0 | 978.4 | 825.5 | 360 | 44.30 |
Dataset Information Summary¶
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1030 entries, 0 to 1029 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Cement (component 1)(kg in a m^3 mixture) 1030 non-null float64 1 Blast Furnace Slag (component 2)(kg in a m^3 mixture) 1030 non-null float64 2 Fly Ash (component 3)(kg in a m^3 mixture) 1030 non-null float64 3 Water (component 4)(kg in a m^3 mixture) 1030 non-null float64 4 Superplasticizer (component 5)(kg in a m^3 mixture) 1030 non-null float64 5 Coarse Aggregate (component 6)(kg in a m^3 mixture) 1030 non-null float64 6 Fine Aggregate (component 7)(kg in a m^3 mixture) 1030 non-null float64 7 Age (day) 1030 non-null int64 8 Concrete compressive strength(MPa, megapascals) 1030 non-null float64 dtypes: float64(8), int64(1) memory usage: 72.5 KB
Statistical Summary of the Dataset¶
df.describe()
| Cement (component 1)(kg in a m^3 mixture) | Blast Furnace Slag (component 2)(kg in a m^3 mixture) | Fly Ash (component 3)(kg in a m^3 mixture) | Water (component 4)(kg in a m^3 mixture) | Superplasticizer (component 5)(kg in a m^3 mixture) | Coarse Aggregate (component 6)(kg in a m^3 mixture) | Fine Aggregate (component 7)(kg in a m^3 mixture) | Age (day) | Concrete compressive strength(MPa, megapascals) | |
|---|---|---|---|---|---|---|---|---|---|
| count | 1030.000000 | 1030.000000 | 1030.000000 | 1030.000000 | 1030.000000 | 1030.000000 | 1030.000000 | 1030.000000 | 1030.000000 |
| mean | 281.167864 | 73.895825 | 54.188350 | 181.567282 | 6.204660 | 972.918932 | 773.580485 | 45.662136 | 35.817961 |
| std | 104.506364 | 86.279342 | 63.997004 | 21.354219 | 5.973841 | 77.753954 | 80.175980 | 63.169912 | 16.705742 |
| min | 102.000000 | 0.000000 | 0.000000 | 121.800000 | 0.000000 | 801.000000 | 594.000000 | 1.000000 | 2.330000 |
| 25% | 192.375000 | 0.000000 | 0.000000 | 164.900000 | 0.000000 | 932.000000 | 730.950000 | 7.000000 | 23.710000 |
| 50% | 272.900000 | 22.000000 | 0.000000 | 185.000000 | 6.400000 | 968.000000 | 779.500000 | 28.000000 | 34.445000 |
| 75% | 350.000000 | 142.950000 | 118.300000 | 192.000000 | 10.200000 | 1029.400000 | 824.000000 | 56.000000 | 46.135000 |
| max | 540.000000 | 359.400000 | 200.100000 | 247.000000 | 32.200000 | 1145.000000 | 992.600000 | 365.000000 | 82.600000 |
Correlation Matrix Analysis¶
df.corr()
| Cement (component 1)(kg in a m^3 mixture) | Blast Furnace Slag (component 2)(kg in a m^3 mixture) | Fly Ash (component 3)(kg in a m^3 mixture) | Water (component 4)(kg in a m^3 mixture) | Superplasticizer (component 5)(kg in a m^3 mixture) | Coarse Aggregate (component 6)(kg in a m^3 mixture) | Fine Aggregate (component 7)(kg in a m^3 mixture) | Age (day) | Concrete compressive strength(MPa, megapascals) | |
|---|---|---|---|---|---|---|---|---|---|
| Cement (component 1)(kg in a m^3 mixture) | 1.000000 | -0.275216 | -0.397467 | -0.081587 | 0.092386 | -0.109349 | -0.222718 | 0.081946 | 0.497832 |
| Blast Furnace Slag (component 2)(kg in a m^3 mixture) | -0.275216 | 1.000000 | -0.323580 | 0.107252 | 0.043270 | -0.283999 | -0.281603 | -0.044246 | 0.134829 |
| Fly Ash (component 3)(kg in a m^3 mixture) | -0.397467 | -0.323580 | 1.000000 | -0.256984 | 0.377503 | -0.009961 | 0.079108 | -0.154371 | -0.105755 |
| Water (component 4)(kg in a m^3 mixture) | -0.081587 | 0.107252 | -0.256984 | 1.000000 | -0.657533 | -0.182294 | -0.450661 | 0.277618 | -0.289633 |
| Superplasticizer (component 5)(kg in a m^3 mixture) | 0.092386 | 0.043270 | 0.377503 | -0.657533 | 1.000000 | -0.265999 | 0.222691 | -0.192700 | 0.366079 |
| Coarse Aggregate (component 6)(kg in a m^3 mixture) | -0.109349 | -0.283999 | -0.009961 | -0.182294 | -0.265999 | 1.000000 | -0.178481 | -0.003016 | -0.164935 |
| Fine Aggregate (component 7)(kg in a m^3 mixture) | -0.222718 | -0.281603 | 0.079108 | -0.450661 | 0.222691 | -0.178481 | 1.000000 | -0.156095 | -0.167241 |
| Age (day) | 0.081946 | -0.044246 | -0.154371 | 0.277618 | -0.192700 | -0.003016 | -0.156095 | 1.000000 | 0.328873 |
| Concrete compressive strength(MPa, megapascals) | 0.497832 | 0.134829 | -0.105755 | -0.289633 | 0.366079 | -0.164935 | -0.167241 | 0.328873 | 1.000000 |
Here's a brief explanation of the results:
- Cement has a strong positive correlation with concrete compressive strength, meaning as the amount of cement increases, the strength tends to increase as well.
- Blast Furnace Slag and Fly Ash have negative correlations with cement, suggesting that as the proportion of these materials increases, the amount of cement typically decreases.
- Water has a notably strong negative correlation with superplasticizer, which makes sense because superplasticizers are used to reduce water content while maintaining workability.
- Superplasticizer has a strong positive correlation with concrete strength, indicating that its usage is beneficial for the strength of the concrete.
- Coarse Aggregate and Fine Aggregate show very little correlation with concrete strength, suggesting that they might not be significant predictors of strength in this mix.
- The Age of the concrete has a positive correlation with strength, which is expected as concrete generally continues to cure and gain strength over time.
Data Cleaning and Quality Assurance¶
After identifying the relevant parameters, our data cleaning process commences. Initially, we eliminate observations with missing values, followed by the removal of duplicate entries. However, it's essential to exercise caution before discarding these data points. We should ascertain whether missing values were genuinely absent or not provided for specific reasons. Similarly, for duplicates, we must investigate why certain observations were duplicated. Removing them should be contingent upon understanding these nuances and circumstances.
Creating a Backup Copy of the Dataframe for Cross-Checking¶
df2=df.copy()
Counting Missing Values in the Dataframe¶
df.isnull().sum()
Cement (component 1)(kg in a m^3 mixture) 0 Blast Furnace Slag (component 2)(kg in a m^3 mixture) 0 Fly Ash (component 3)(kg in a m^3 mixture) 0 Water (component 4)(kg in a m^3 mixture) 0 Superplasticizer (component 5)(kg in a m^3 mixture) 0 Coarse Aggregate (component 6)(kg in a m^3 mixture) 0 Fine Aggregate (component 7)(kg in a m^3 mixture) 0 Age (day) 0 Concrete compressive strength(MPa, megapascals) 0 dtype: int64
Counting Duplicate Rows in the Dataframe
df.duplicated().sum()
25
Removing Missing Values and Duplicate Rows from the Dataframe¶
df.dropna(inplace=True)
df.drop_duplicates(inplace=True)
Shortening Column Names for Clarity¶
name=[]
for i,j in enumerate(df.columns):
name.append(j.split('(')[0])
Creating a Copy of the Dataframe with Modified Column Names¶
df2=df.copy()
df2.columns=name
Boxplot Visualization of Melted Data¶
df_melted = df2.melt()
plt.figure(figsize=(15,6))
ax = plt.axes()
# Use a color palette to assign different colors to each boxplot
box_plot = sns.boxplot(x='variable', y='value', data=df_melted, palette='Set3') # 'Set3' is an example of a qualitative color palette suitable for categorical data
# Uncomment to add a legend if needed
# box_plot.legend(df2.columns, bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
plt.show()
/var/folders/7j/czphbg2d2_v9hhk42wc_q6dh0000gn/T/ipykernel_30126/3114610049.py:6: FutureWarning: Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect. box_plot = sns.boxplot(x='variable', y='value', data=df_melted, palette='Set3') # 'Set3' is an example of a qualitative color palette suitable for categorical data
Here's a brief analysis:
- Cement has a wide range of values with a high median, suggesting that the amount used varies significantly across mixes.
- Blast Furnace Slag and Fly Ash show lower medians and tighter distributions, indicating less variability and lower usage quantities in the mixtures.
- Water has a tight distribution around a central median, suggesting consistent usage across mixes.
- Superplasticizer appears to have a tight distribution with some outliers, indicating that while it's generally used in consistent amounts, there are cases where it's used much more or much less.
- Coarse Aggregate and Fine Aggregate have wide ranges but fairly central medians, indicating variability in usage.
- Age shows a skewed distribution with outliers, suggesting that while most of the concrete samples are of a certain age range, there are some significantly older samples.
- Concrete compressive strength shows a wide range of values, which is critical for understanding how the other components affect the strength of the concrete.
The boxplot reveals the presence of outliers within the dataset.
Outlier Removal Using the Interquartile Range (IQR) Method¶
def remove_outliers(df):
q1 = df.quantile(0.25)
q3 = df.quantile(0.75)
iqr = q3 - q1
dfr = df[~((df < (q1 - 1.5 * iqr))).any(axis=1)]
dfr2 = dfr[~((dfr > (q3 + 1.5 * iqr)).any(axis=1))]
return dfr2
df2 = remove_outliers(df2)
df2 = remove_outliers(df2)
Exporting Cleaned Data to Excel¶
df.shape
(1005, 9)
df2.shape
(776, 9)
df
| Cement (component 1)(kg in a m^3 mixture) | Blast Furnace Slag (component 2)(kg in a m^3 mixture) | Fly Ash (component 3)(kg in a m^3 mixture) | Water (component 4)(kg in a m^3 mixture) | Superplasticizer (component 5)(kg in a m^3 mixture) | Coarse Aggregate (component 6)(kg in a m^3 mixture) | Fine Aggregate (component 7)(kg in a m^3 mixture) | Age (day) | Concrete compressive strength(MPa, megapascals) | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 540.0 | 0.0 | 0.0 | 162.0 | 2.5 | 1040.0 | 676.0 | 28 | 79.99 |
| 1 | 540.0 | 0.0 | 0.0 | 162.0 | 2.5 | 1055.0 | 676.0 | 28 | 61.89 |
| 2 | 332.5 | 142.5 | 0.0 | 228.0 | 0.0 | 932.0 | 594.0 | 270 | 40.27 |
| 3 | 332.5 | 142.5 | 0.0 | 228.0 | 0.0 | 932.0 | 594.0 | 365 | 41.05 |
| 4 | 198.6 | 132.4 | 0.0 | 192.0 | 0.0 | 978.4 | 825.5 | 360 | 44.30 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1025 | 276.4 | 116.0 | 90.3 | 179.6 | 8.9 | 870.1 | 768.3 | 28 | 44.28 |
| 1026 | 322.2 | 0.0 | 115.6 | 196.0 | 10.4 | 817.9 | 813.4 | 28 | 31.18 |
| 1027 | 148.5 | 139.4 | 108.6 | 192.7 | 6.1 | 892.4 | 780.0 | 28 | 23.70 |
| 1028 | 159.1 | 186.7 | 0.0 | 175.6 | 11.3 | 989.6 | 788.9 | 28 | 32.77 |
| 1029 | 260.9 | 100.5 | 78.3 | 200.6 | 8.6 | 864.5 | 761.5 | 28 | 32.40 |
1005 rows × 9 columns
Renaming DataFrame Columns Based on a Mapping¶
print(df2.columns)
Index(['Cement ', 'Blast Furnace Slag ', 'Fly Ash ', 'Water ',
'Superplasticizer ', 'Coarse Aggregate ', 'Fine Aggregate ', 'Age ',
'Concrete compressive strength'],
dtype='object')
# Step 1: Strip leading and trailing spaces from column names in df2
df2.columns = df2.columns.str.strip()
# Step 2: Update the column mapping dictionary to match the stripped column names accurately
column_mapping = {
'Concrete compressive strength': 'Strength', # Removed the extra spaces and any other format issues
'Cement': 'Cement_content' # Assuming 'Cement (component 1)(kg in a m^3 mixture)' also had spaces trimmed
}
# Step 3: Apply the renaming to df2 using the updated column mapping
df2 = df2.rename(columns=column_mapping)
df2
| Cement_content | Blast Furnace Slag | Fly Ash | Water | Superplasticizer | Coarse Aggregate | Fine Aggregate | Age | Strength | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 540.0 | 0.0 | 0.0 | 162.0 | 2.5 | 1055.0 | 676.0 | 28 | 61.89 |
| 8 | 266.0 | 114.0 | 0.0 | 228.0 | 0.0 | 932.0 | 670.0 | 28 | 45.85 |
| 11 | 198.6 | 132.4 | 0.0 | 192.0 | 0.0 | 978.4 | 825.5 | 28 | 28.02 |
| 14 | 304.0 | 76.0 | 0.0 | 228.0 | 0.0 | 932.0 | 670.0 | 28 | 47.81 |
| 21 | 139.6 | 209.4 | 0.0 | 192.0 | 0.0 | 1047.0 | 806.9 | 28 | 28.24 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1025 | 276.4 | 116.0 | 90.3 | 179.6 | 8.9 | 870.1 | 768.3 | 28 | 44.28 |
| 1026 | 322.2 | 0.0 | 115.6 | 196.0 | 10.4 | 817.9 | 813.4 | 28 | 31.18 |
| 1027 | 148.5 | 139.4 | 108.6 | 192.7 | 6.1 | 892.4 | 780.0 | 28 | 23.70 |
| 1028 | 159.1 | 186.7 | 0.0 | 175.6 | 11.3 | 989.6 | 788.9 | 28 | 32.77 |
| 1029 | 260.9 | 100.5 | 78.3 | 200.6 | 8.6 | 864.5 | 761.5 | 28 | 32.40 |
776 rows × 9 columns
df2.to_excel('cleaned_data.xlsx', index=False)
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from scipy.stats import gaussian_kde, linregress
# Load the data
cleaned_data = pd.read_excel('cleaned_data.xlsx', engine='openpyxl')
# Create a figure with a grid of subplots
fig, axs = plt.subplots(2, 3, figsize=(15, 10), dpi=1200) # High-resolution for printing
# Define a function to plot histograms with a scaled density curve
def plot_hist_with_density(ax, data, bins, color, title, xlabel, ylabel):
# Plot histogram with raw counts
count, bins, ignored = ax.hist(data, bins=bins, density=False, alpha=0.7, color=color, edgecolor='black')
# Calculate bin width
bin_width = bins[1] - bins[0]
# Perform KDE
kde = gaussian_kde(data)
x = np.linspace(bins[0], bins[-1], 300)
# Scale the KDE to match the histogram counts
scaled_kde = kde(x) * len(data) * bin_width
ax.plot(x, scaled_kde, color='red', linewidth=2) # Scaled density curve in red
# Set plot titles and labels with bold fonts
ax.set_title(title, fontweight='bold', fontsize=16)
ax.set_xlabel(xlabel, fontweight='bold', fontsize=14)
ax.set_ylabel(ylabel, fontweight='bold', fontsize=14)
# Set tick label font weight to bold and adjust tick parameters
ax.tick_params(axis='both', which='both', labelsize=12, width=2)
for label in ax.get_xticklabels():
label.set_fontweight('bold')
for label in ax.get_yticklabels():
label.set_fontweight('bold')
ax.grid(False) # Remove grid
# Define a function to plot scatter plots with a regression line
def plot_scatter_with_regression(ax, x, y, color, title, xlabel, ylabel):
ax.scatter(x, y, color=color, alpha=0.5)
slope, intercept, r_value, p_value, std_err = linregress(x, y)
# Plot regression line
ax.plot(x, intercept + slope * x, color='black', label=f'y={slope:.2f}x+{intercept:.2f}')
ax.set_title(title, fontweight='bold', fontsize=16)
ax.set_xlabel(xlabel, fontweight='bold', fontsize=14)
ax.set_ylabel(ylabel, fontweight='bold', fontsize=14)
# Set tick label font weight to bold and adjust tick parameters
ax.tick_params(axis='both', which='both', labelsize=12, width=2)
for label in ax.get_xticklabels():
label.set_fontweight('bold')
for label in ax.get_yticklabels():
label.set_fontweight('bold')
ax.legend(fontsize=12)
ax.grid(False) # Remove grid
# Configure plots with updated histogram function
plot_hist_with_density(
axs[0, 0],
cleaned_data['Cement_content'],
bins=20,
color='blue',
title='Cement Content Frequency',
xlabel='Cement Content',
ylabel='Count'
)
plot_hist_with_density(
axs[0, 1],
cleaned_data['Water'],
bins=20,
color='green',
title='Water Content Frequency',
xlabel='Water Content',
ylabel='Count'
)
plot_scatter_with_regression(
axs[0, 2],
cleaned_data['Water'],
cleaned_data['Superplasticizer'],
color='blue',
title='Superplasticizer vs. Water Content',
xlabel='Water Content',
ylabel='Superplasticizer'
)
plot_scatter_with_regression(
axs[1, 0],
cleaned_data['Cement_content'],
cleaned_data['Strength'],
color='darkblue',
title='Strength vs. Cement Content',
xlabel='Cement Content',
ylabel='Strength'
)
plot_hist_with_density(
axs[1, 1],
cleaned_data['Age'],
bins=20,
color='orange',
title='Age Frequency',
xlabel='Age (days)',
ylabel='Count'
)
plot_scatter_with_regression(
axs[1, 2],
cleaned_data['Age'],
cleaned_data['Strength'],
color='darkblue',
title='Strength vs. Age',
xlabel='Age (days)',
ylabel='Strength'
)
# Adjust layout to avoid overlap and ensure everything fits well
fig.tight_layout(pad=3.0)
# Show the plot
plt.show()